package com.econage.eccm.test.base.webdatasource.web;

import com.econage.eccm.test.base.webdatasource.DataSourceConfiguration;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import org.apache.log4j.Logger;

import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.*;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

public class AppDataSourceContext {
    public static final String VERSION = "20171216";

    private static final Logger log = Logger.getLogger(AppDataSourceContext.class);

    private final Properties baseProperties;
    private final DataSourceConfiguration configuration;
    private DataSource dataSource;

    private Constructor<?> dataSourceConstructor;

    public AppDataSourceContext() throws NoSuchMethodException, SQLException, IOException {
        this(null);
    }

    public AppDataSourceContext(Properties baseProperties) throws NoSuchMethodException, SQLException, IOException {
        if(baseProperties==null){
            baseProperties = parseDefaultDbProperties();
        }

        this.baseProperties = baseProperties;
        this.configuration = parseConfiguration(this.baseProperties);
        this.dataSource = this.configuration.buildUnderlyingDataSource(this.baseProperties);

        this.dataSourceConstructor = Proxy.getProxyClass(
                AppDataSourceContext.class.getClassLoader(),
                    new Class[] {javax.sql.DataSource.class}
        ).getConstructor(new Class[] { InvocationHandler.class });
    }

    public Properties parseDefaultDbProperties() throws IOException {
        InputStream in	 = null;
        try{
            in = AppDataSourceContext.class.getClassLoader().getResourceAsStream("DbConnectionService.properties");
            Properties pros = new Properties();
            pros.load(in);
            return pros;
        } finally{
            if(in!=null) {
                try { in.close(); } catch (Exception e) { }
            }
        }
    }


    protected static final String PROP_TRACECONNECTIONCREATION = "webDataSource.traceConnectionCreation";
    protected static final String PROP_TRACESTATEMENTCREATION = "webDataSource.traceStatementCreation";
    protected static final String PROP_TRACERESULTSETCREATION = "webDataSource.traceResultSetCreation";

    protected static final String PROP_STATEMENTLEAKWARING = "webDataSource.statementLeakWaring";
    protected static final String PROP_RESULTSETLEAKWARING = "webDataSource.resultSetLeakWaring";

    protected static final String PROP_AUTOCOMMITFALSEFORCE = "webDataSource.autoCommitFalseForce";

    protected static final String PROP_COMMITBEFORECLOSE = "webDataSource.commitBeforeClose";
    protected static final String PROP_IGNORECOMMITCHANGE = "webDataSource.ignoreCommitChange";
    protected static final String PROP_IGNORETRANSACTIONCHANGE = "webDataSource.ignoreTransactionChange";

    private DataSourceConfiguration parseConfiguration(Properties properties) throws NoSuchMethodException {
        DataSourceConfiguration configuration = new DataSourceConfiguration();
        String value = null;

        value = properties.getProperty(PROP_TRACECONNECTIONCREATION);
        if(value!=null){
            configuration.setTraceConnectionCreation(Boolean.parseBoolean(value));
        }
        value = properties.getProperty(PROP_TRACESTATEMENTCREATION);
        if(value!=null){
            configuration.setTraceStatementCreation(Boolean.parseBoolean(value));
        }
        value = properties.getProperty(PROP_TRACERESULTSETCREATION);
        if(value!=null){
            configuration.setTraceResultSetCreation(Boolean.parseBoolean(value));
        }

        value = properties.getProperty(PROP_STATEMENTLEAKWARING);
        if(value!=null){
            configuration.setStatementLeakWaring(Boolean.parseBoolean(value));
        }
        value = properties.getProperty(PROP_RESULTSETLEAKWARING);
        if(value!=null){
            configuration.setResultSetLeakWaring(Boolean.parseBoolean(value));
        }

        value = properties.getProperty(PROP_AUTOCOMMITFALSEFORCE);
        if(value!=null){
            configuration.setAutoCommitFalseForce(Boolean.parseBoolean(value));
        }

        value = properties.getProperty(PROP_COMMITBEFORECLOSE);
        if(value!=null){
            configuration.setCommitBeforeClose(Boolean.parseBoolean(value));
        }
        value = properties.getProperty(PROP_IGNORECOMMITCHANGE);
        if(value!=null){
            configuration.setIgnoreCommitChange(Boolean.parseBoolean(value));
        }
        value = properties.getProperty(PROP_IGNORETRANSACTIONCHANGE);
        if(value!=null){
            configuration.setIgnoreTransactionChange(Boolean.parseBoolean(value));
        }

        return configuration;
    }

    public String getDbUrl(){
        return baseProperties.getProperty("url");
    }


    private final ThreadLocal<Boolean> localCloseForce = new ThreadLocal<Boolean>(){
        @Override
        protected Boolean initialValue() {
            return Boolean.FALSE;
        }
    };
    private void runningCloseConnectionForce(){
        localCloseForce.set(Boolean.TRUE);
    }
    public boolean isRunningCloseConnectionForce(){
        return localCloseForce.get();
    }


    private final ThreadLocal<Boolean> localAllocationRegistration = new ThreadLocal<Boolean>(){
        @Override
        protected Boolean initialValue() {
            return Boolean.FALSE;
        }
    };
    void register4Access(){
        localAllocationRegistration.set(Boolean.TRUE);
    }
    private boolean haveRegistered(){
        return localAllocationRegistration.get();
    }


    private final ThreadLocal<ConnectionEntry> localConnection = new ThreadLocal<ConnectionEntry>();
    private final ConcurrentHashMap<ConnectionEntry,Integer> connCacheMap = new ConcurrentHashMap<ConnectionEntry,Integer>();
    public Connection fetchConnection() throws SQLException {
        if(!haveRegistered()){
            throw new SQLException("编程错误，未注册连接获取功能！");
        }
        try {
            if(localConnection.get()==null){
                Connection dbConnFacade = configuration.newConnectionFacade(dataSource.getConnection(),this);
                localConnection.set(new ConnectionEntry(dbConnFacade));
                connCacheMap.put(localConnection.get(),0);
            }
            return localConnection.get().getConnection();
        } catch (Exception e) {
            throw new SQLException(e);
        }

    }

    void closeForce() throws SQLException {
        try{
            runningCloseConnectionForce();
            ConnectionEntry localConnectionEntry = localConnection.get();
            if(localConnectionEntry!=null){
                try{
                    //可能报错，嵌套一层try-finally
                    try{
                        if(configuration.isCommitBeforeClose()){
                            localConnectionEntry.getConnection().commit();
                        }
                    }finally{
                        localConnectionEntry.getConnection().close();
                    }
                }finally{
                    localConnection.remove();
                    connCacheMap.remove(localConnectionEntry);
                }
            }
        }finally{
            localCloseForce.remove();
            localAllocationRegistration.remove();
        }
    }

    protected class ConnectionEntry {
        private Connection connection;
        private Throwable allocationStack;

        public ConnectionEntry(Connection connection) {
            this.connection = connection;
            if (AppDataSourceContext.this.configuration.isTraceConnectionCreation()) {
                this.allocationStack = new Throwable();
            }
        }

        public Connection getConnection() {
            return connection;
        }

        public Throwable getAllocationStack() {
            return allocationStack;
        }
    }

    //获取所有运行中，未归还的连接调用栈信息
    public List<StackTraceElement[]> getRunningConnectionStacks(){
        return FluentIterable.from(connCacheMap.keySet())
                .filter(new Predicate<ConnectionEntry>() {
                    @Override
                    public boolean apply(ConnectionEntry input) {
                        return input.getAllocationStack()!=null;
                    }
                })
                .transform(new Function<ConnectionEntry, StackTraceElement[]>() {
                    @Override
                    public StackTraceElement[] apply(ConnectionEntry input) {
                        return input.getAllocationStack().getStackTrace();
                    }
                }).toList();
    }

}
